﻿#Region "Imports"
Imports System.Activities
Imports System.Activities.XamlIntegration
Imports System.Threading
Imports System.ServiceModel
Imports System.ServiceModel.Channels
Imports System.Activities.Tracking
Imports System.Activities.Presentation
Imports KumaSystem.WFDesignerExpress.TraceSendService
Imports System.Globalization
Imports System.Xaml

#End Region

Module ConsoleExecuter

#Region "定数定義"
    Private Const PLUGIN_FOLDERNAME = "Plugin"

    Private Const WFFILE_ARGS As String = "wfile"
    Private Const WFTRACE_ARGS As String = "usetrace"
    Private Const WFPLUGIN_ARGS As String = "plugin"
    Private Const WFRESULTARGNAME_ARGS As String = "resultarg"
    Private Const WFLASTPAUSE_ARGS As String = "wait"

    Private Const WFHELP_ARGS As String = "help"
    Private Const WFHELP_ARGS2 As String = "?"

#End Region

#Region "変数定義"
    Private _trcService As New TraceReceiver
#End Region

#Region "メソッド定義"
    ''' <summary>メインプロシージャ</summary>
    ''' <param name="args">
    ''' </param>
    ''' <returns></returns>
    ''' <remarks>
    ''' wfile:実行するワークフローファイル(.xaml)の指定<br />
    ''' [引数名]:ワークフローの引数に渡す値<br />
    ''' usetrace:デザイナへのトレース情報送信を行う(True で行う。False で行わない）<br />
    ''' </remarks>
    Function Main(ByVal args As String()) As Integer

        If (args Is Nothing) OrElse (args.Length <= 0) Then Return 0
        Dim argument = ReplaceArguments(args)

        If GetDisplayHelpOption(argument) Then
            ShowCommandReference()
            Return 0
        End If

        'ワークフローファイルのチェック
        Dim target = GetWorkflowFilename(argument)
        target = IO.Path.GetFullPath(target)
        If Not CheckWorkflowFile(target) Then
            Console.WriteLine(GetLocalizedString("IllegalFile"))
            Return 0
        End If
        'WCF トレースを利用するか
        Dim traceFlag = GetUseTraceOption(argument)
        'プラグインフォルダの取得
        Dim pluginFolderName = GetPlugiinFolderOption(argument)
        '返却用のワークフロー引数を利用するか取得
        Dim resArgName = GetUseResultArgument(argument).Trim
        '終了時に一時停止にするか取得
        Dim usePause = GetUseLastPauseOption(argument)

        'Plugin フォルダよりコピー・ロード
        LoadPlugin(pluginFolderName)

        'ワークフローの引数設定
        Dim wfFile As Activity = Nothing
        Dim wfArgs As Dictionary(Of String, Object) = Nothing
        Try
            wfFile = XamlServices.Load(ActivityXamlServices.CreateReader(
                              New XamlXmlReader(target,
                              New XamlXmlReaderSettings With {.LocalAssembly = Reflection.Assembly.GetExecutingAssembly}
                              )))

            'wfFile = ActivityXamlServices.Load(target)
            '引数が存在するかチェック
            Dim dynAct = TryCast(wfFile, DynamicActivity)
            If (dynAct IsNot Nothing) Then
                For Each child In SetWorkflowArgument(argument).Keys()
                    If Not CheckWorkflowArgument(dynAct, child) Then Continue For

                    If wfArgs Is Nothing Then wfArgs = New Dictionary(Of String, Object)
                    wfArgs.Add(child, GetArgumentValue(argument, child))
                Next
            End If
        Catch ex As Exception
            Console.WriteLine(ex.Message)
        End Try
        If wfFile Is Nothing Then
            Console.WriteLine(GetLocalizedString("ReadError"))
            Return 0
        End If

        'ワークフローの実行
        Dim resultValue As Integer = 0
        Dim execResult As Integer = 0
        Dim syncEvent As New AutoResetEvent(False)
        Try
            Dim wfApps As WorkflowApplication = Nothing
            If wfArgs Is Nothing Then
                wfApps = New WorkflowApplication(wfFile)
            Else
                wfApps = New WorkflowApplication(wfFile, wfArgs)
            End If
            wfApps.SynchronizationContext = SynchronizationContext.Current
            'WorkflowApplication のイベント処理を設定
            wfApps.Aborted = Sub(e As WorkflowApplicationAbortedEventArgs)
                                 Console.WriteLine(e.Reason)
                                 syncEvent.Set()
                             End Sub
            wfApps.OnUnhandledException = Function(e As WorkflowApplicationUnhandledExceptionEventArgs)
                                              Console.WriteLine(e.UnhandledException)
                                              Return UnhandledExceptionAction.Terminate
                                          End Function
            wfApps.Completed = Sub(e As WorkflowApplicationCompletedEventArgs)
                                   If (e.CompletionState <> ActivityInstanceState.Canceled) AndAlso
                                       (e.CompletionState <> ActivityInstanceState.Faulted) AndAlso
                                       (resArgName <> "") Then
                                       '結果を引数から取得
                                       If e.Outputs.ContainsKey(resArgName) Then
                                           Dim resVal = e.Outputs(resArgName)
                                           If IsNumeric(resVal) Then resultValue = Integer.Parse(resVal.ToString)
                                       End If
                                   End If
                                   syncEvent.Set()
                               End Sub

            'トラッキングプロファイルの生成
            If traceFlag Then
                Dim customTracker = New VisualTrackingParticipant
                customTracker.TrackingProfile = _trcService.CreateTrackingProfile()
                customTracker.ActivityIdToWorkflowElementMap = _trcService.ActivityIdDict
                'customTracker.ActivityElementMap = mdlDict
                'トラッキングレコード受信時のイベントトラップ
                AddHandler customTracker.TrackingRecordReceived, AddressOf OnTrackingRecordReceived
                wfApps.Extensions.Add(customTracker)
            End If

            ConnectService(traceFlag)
            wfApps.Run()
            syncEvent.WaitOne()
            DisConnectService(traceFlag)

            If resultValue <> 0 Then
                execResult = resultValue
            Else
                execResult = 1
            End If
        Catch ex As Exception
            Console.WriteLine(ex.Message)
            Console.WriteLine(AppDomain.CurrentDomain.GetAssemblies.ToString)
        Finally
            If usePause Then
                Console.WriteLine(GetLocalizedString("PauseMessage"))
                Console.ReadKey()
            End If
        End Try

        Return execResult

    End Function

    ''' <summary>Plugin フォルダに存在する DLL を全てロードし、リソースを抽出する</summary>
    Private Sub LoadActivities(ByVal newPluginFolder As String)
        'Plugin フォルダに存在する DLL を全てロード
        For Each childDll In IO.Directory.GetFiles(newPluginFolder, "*.dll")
            Dim childAsm = Reflection.Assembly.LoadFrom(childDll)
            'IRegisterMetadata.Register を呼ぶ
            Dim meta = childAsm.GetTypes().Where(Function(t)
                                                     Return GetType(Metadata.IRegisterMetadata).IsAssignableFrom(t) AndAlso
                                                               Not t.IsAbstract AndAlso t.IsVisible
                                                 End Function)
            Dim validReference As Boolean = False
            For Each childMeta In meta
                Dim metadata = TryCast(Activator.CreateInstance(childMeta), Metadata.IRegisterMetadata)
                If metadata IsNot Nothing Then
                    metadata.Register()
                    validReference = True
                End If
            Next

            For Each refAsm In childAsm.GetReferencedAssemblies
                Dim targetAsm = refAsm
                If AppDomain.CurrentDomain.GetAssemblies.Where(Function(t) t.FullName = targetAsm.FullName).Count <= 0 Then
                    Reflection.Assembly.Load(refAsm)

                End If
            Next

        Next
    End Sub

    ''' <summary>プラグインフォルダからの DLL 読み込み</summary>
    Private Sub LoadPlugin(ByVal folderName As String)
        If folderName = "" Then
            LoadActivities(My.Application.Info.DirectoryPath)
            Return
        End If
        'If Not IO.Directory.Exists(IO.Path.Combine(My.Application.Info.DirectoryPath, folderName)) Then
        '    IO.Directory.CreateDirectory(IO.Path.Combine(My.Application.Info.DirectoryPath, folderName))
        'End If
        For Each childFolder In My.Computer.FileSystem.GetDirectories(IO.Path.Combine(My.Application.Info.DirectoryPath, folderName), FileIO.SearchOption.SearchAllSubDirectories)
            'AppDomain の検索フォルダにプラグインフォルダを指定
            AppDomain.CurrentDomain.AppendPrivatePath(childFolder)
        Next
        For Each childFolder In My.Computer.FileSystem.GetDirectories(IO.Path.Combine(My.Application.Info.DirectoryPath, folderName), FileIO.SearchOption.SearchAllSubDirectories)
            Dim newPluginFolder = childFolder
            'Plugin フォルダから DLL をロードし、リソースを抽出
            LoadActivities(childFolder)
        Next
    End Sub

#End Region

#Region "コマンドライン関係"
    ''' <summary>コマンドライン引数からのワークフローファイル名取得</summary>
    Private Function GetWorkflowFilename(ByVal args As String()) As String
        Dim result = ""
        Dim argValue = "/" + WFFILE_ARGS + ":"
        Dim r = From x In args
                Where x.StartsWith(argValue)
                Select x.Replace(argValue, "")

        If (r IsNot Nothing) AndAlso (r.Count > 0) AndAlso (r.First <> "") Then result = r.First
        Return result
    End Function

    ''' <summary>コマンドライン引数からのトレース機能利用フラグ取得</summary>
    Private Function GetUseTraceOption(ByVal args As String()) As Boolean
        Dim result = False
        Dim getValue = ""
        Dim argValue = "/" + WFTRACE_ARGS + ":"
        Dim r = From x In args
                Where x.StartsWith(argValue)
                Select x.Replace(argValue, "")

        If (r IsNot Nothing) AndAlso (r.Count > 0) AndAlso (r.First <> "") Then getValue = r.First
        If (getValue.ToLower = "true") Then result = True

        Return result
    End Function

    ''' <summary>コマンドライン引数からのプラグインフォルダ取得</summary>
    Private Function GetPlugiinFolderOption(ByVal args As String()) As String
        Dim result = ""
        Dim argValue = "/" + WFPLUGIN_ARGS + ":"
        Dim r = From x In args
                Where x.StartsWith(argValue)
                Select x.Replace(argValue, "")

        If (r IsNot Nothing) AndAlso (r.Count > 0) AndAlso (r.First <> "") Then result = r.First
        Return result
    End Function

    ''' <summary>コマンドライン引数からの返却用ワークフロー引数利用設定の取得</summary>
    Private Function GetUseResultArgument(ByVal args As String()) As String
        Dim result = ""
        Dim getValue = ""
        Dim argValue = "/" + WFRESULTARGNAME_ARGS + ":"
        Dim r = From x In args
                Where x.StartsWith(argValue)
                Select x.Replace(argValue, "")

        If (r IsNot Nothing) AndAlso (r.Count > 0) AndAlso (r.First <> "") Then getValue = r.First

        Return result
    End Function

    ''' <summary>コマンドライン引数からの終了時一時待機利用フラグ取得</summary>
    Private Function GetUseLastPauseOption(ByVal args As String()) As Boolean
        Dim result = False
        Dim getValue = ""
        Dim argValue = "/" + WFLASTPAUSE_ARGS + ":"
        Dim r = From x In args
                Where x.StartsWith(argValue)
                Select x.Replace(argValue, "")

        If (r IsNot Nothing) AndAlso (r.Count > 0) AndAlso (r.First <> "") Then getValue = r.First
        If (getValue.ToLower = "true") Then result = True

        Return result
    End Function

    ''' <summary>ヘルプを表示する必要があるかを取得</summary>
    Private Function GetDisplayHelpOption(ByVal args As String()) As Boolean
        Dim result = False
        Dim getValue = ""
        Dim argValue = "/" + WFHELP_ARGS
        Dim argValue2 = "/" + WFHELP_ARGS2
        Dim r = From x In args
                Where x.StartsWith(argValue) OrElse x.StartsWith(argValue2)
                Select x.Replace(argValue, "")

        If (r IsNot Nothing) AndAlso (r.Count > 0) AndAlso (r.First <> "") Then result = True

        Return result
    End Function

    ''' <summary>コマンドラインオプションの統一</summary>
    Private Function ReplaceArguments(ByVal args As String()) As String()
        Dim resultArgs As String() = args
        For index = 0 To args.Length - 1
            Dim childArgs = args(index)
            If Not childArgs.StartsWith("-") Then Continue For

            childArgs = "/" & childArgs.Substring(1)
            If childArgs.Substring(1, 5).ToLower = WFFILE_ARGS Then
                childArgs = childArgs.Substring(0, 1) + WFFILE_ARGS + childArgs.Substring(6)
            End If
            resultArgs(index) = childArgs
        Next

        Return resultArgs
    End Function

    ''' <summary>ワークフローファイルのチェック</summary>
    Private Function CheckWorkflowFile(ByVal filename As String) As Boolean
        Dim result = (filename.Trim <> "") AndAlso
                           (System.IO.File.Exists(filename))
        Return result
    End Function

    ''' <summary>ワークフローの引数を設定</summary>
    Private Function SetWorkflowArgument(ByVal args As String()) As Dictionary(Of String, Object)
        Dim result As New Dictionary(Of String, Object)
        Dim r = From x In args
                Where x.StartsWith("/")
                Select x.Replace("/", "")

        For Each child In r
            If (Not child.Contains(":")) OrElse
                (child.Contains(WFFILE_ARGS + ":")) Then Continue For

            Dim splitPosition = child.IndexOf(":")
            result.Add(child.Substring(0, splitPosition), child.Substring(splitPosition + 1))
        Next
        Return result
    End Function

    ''' <summary>ワークフローに引数が存在するかチェック</summary>
    Private Function CheckWorkflowArgument(ByVal wf As DynamicActivity, ByVal argsName As String) As Boolean
        Return wf.Properties.Contains(argsName)
    End Function

    ''' <summary>コマンドライン引数から指定した名称の値を取得</summary>
    Private Function GetArgumentValue(ByVal args As String(), ByVal argsName As String) As Object
        Dim r = From x In args
                   Where x.StartsWith("/" + argsName)
                   Select x.Replace("/" + argsName + ":", "")

        Return r.First
    End Function

    ''' <summary>コマンドラインオプションのヘルプを表示</summary>
    Private Sub ShowCommandReference()
        Console.WriteLine(My.Application.Info.AssemblyName + " " + My.Application.Info.Version.ToString)
        Console.WriteLine(GetLocalizedString("help"))
    End Sub

#End Region

#Region "リソース"
    Private Const NATIVECULTURE = "en"
    Private targetCulture As String() = {"en", "ja"}
    Private currentCultureString = CultureInfo.CurrentCulture.Name.Replace("-", "").ToLower.Substring(0, 2)

    ''' <summary>カルチャに対応したリソース文字列を取得</summary>
    Private Function GetLocalizedString(ByVal key As String) As String
        Dim result = ""

        If Not targetCulture.Contains(currentCultureString) Then
            currentCultureString = NATIVECULTURE
        End If

        Return My.Resources.ResourceManager.GetString(key + currentCultureString)
    End Function

#End Region

#Region "WCF によるトレースデータ送信"

    Private _chFactory As ChannelFactory(Of TraceSendService.ITraceService) = Nothing
    Private _chProxy As TraceSendService.ITraceService = Nothing

    Private Function ConnectService(ByVal flag As Boolean) As Boolean
        If Not flag Then Return True
        Dim namedPipe As Binding = New NetNamedPipeBinding(NetNamedPipeSecurityMode.None)
        Dim localAddress As New EndpointAddress(TraceSendService.TraceReceiver.NAMEDPIPE_ADDRESS)

        Dim result As Boolean = False
        Try
            _chFactory = New ChannelFactory(Of TraceSendService.ITraceService)(namedPipe, localAddress)
            _chFactory.Open()
            _chProxy = _chFactory.CreateChannel

            result = True
        Catch ex As Exception
            Console.WriteLine(ex.Message)
            Try
            Finally
                _chFactory.Close()
            End Try
        End Try
        Return result
    End Function

    Private Function SendTraceData(ByVal traceDataArgs As TrackingEventArgs) As Boolean
        Dim result As Boolean = False
        Try
            If _chFactory.State <> CommunicationState.Opened Then Return True

            Dim trackRecord = TryCast(traceDataArgs.Record, ActivityStateRecord)
            If trackRecord Is Nothing Then Return True

            Dim msg1 = trackRecord.Activity.Name + " " + trackRecord.State + ControlChars.NewLine
            Dim msg2 = "--------------------------" + ControlChars.NewLine
            _chProxy.SendTraceData(msg1)
            _chProxy.SendTraceData(msg2)
            result = True
        Catch ex As Exception
            Console.WriteLine(ex.Message)
            Try
            Finally
                _chFactory.Close()
            End Try
        End Try
        Return result
    End Function

    Private Function DisConnectService(ByVal flag As Boolean) As Boolean
        If Not flag Then Return True
        If (_chFactory Is Nothing) Then Return True

        Dim result As Boolean = False
        Try
            _chFactory.Close()
            result = True
        Catch ex As Exception
            Console.WriteLine(ex.Message)
        End Try
        Return result
    End Function

#End Region

#Region "トレース"

    Private Function OnTrackingRecordReceived(ByVal sender As System.Activities.Tracking.TrackingParticipant, ByVal e As TrackingEventArgs)
        SendTraceData(e)
        Return Nothing
    End Function

#End Region

End Module
